package com.vivi.codereview.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.InetAddress;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import com.vivi.codereview.WhatTheFuckException;

public class ZipHandler {

	/**
	 * Clustering for Zip file.
	 */
	protected static final int CLUSTER_ZIP_FILE = 1024;

	/**
	 * Buffer siwe zhile reading a file;
	 */
	protected static final int BUFFER_FILE = 1024;

	private static final String FILE_ALREADY_USED = "The process cannot access the file because it is being used by another process";

	private String logFilename;

	private UserInputHandler inputUtil;

	public ZipHandler(String zipShortFilename, String logFilename) {
		this.logFilename = logFilename;
		this.inputUtil = new UserInputHandler();
	}

	/**
	 * Create a zip file in root location, with all given files into
	 * 
	 * @param root
	 * @param chosenFiles
	 * @throws IOException
	 */
	public void createZipFile(String root, String ptrNumber, Set<String> chosenFiles, String destinationFile)
			throws Exception {
		Boolean hasErrors = null;
		Log.log("We want to zip into " + destinationFile, Log.D);
		File zipFile = new File(destinationFile);
		zipFile = replaceFileIfExists(zipFile);
		Log.log("Creation of this zip file: " + destinationFile, Log.I);
		boolean hasBeenWritten = false;
		do {
			try {
				FileOutputStream fos = new FileOutputStream(zipFile);
				ZipOutputStream zos = new ZipOutputStream(fos);
				hasErrors = false;
				Map<String, String> mapFileStatus = new HashMap<String, String>();
				for (String file : chosenFiles) {
					mapFileStatus.put(file, "not processed");
				}
				for (String filename : chosenFiles) {
					String status = "unknown";
					try {
						addToZipFile(root, filename, zos);
						Log.log("OK to zip one file " + filename, Log.D);
						status = "OK";
					} catch (FileNotFoundException e) {
						Log.log("Not able to zip one file " + filename + e.getMessage(), Log.W);
						hasErrors = true;
						status = "KO " + e.getMessage();
					} catch (UnsupportedOperationException e) {
						Log.log("Not able to zip one file " + filename + e.getMessage(), Log.W);
						hasErrors = true;
						status = "KO " + e.getMessage();
					} catch (IOException e) {
						Log.log("Not able to zip one file " + filename + e.getMessage(), Log.W);
						hasErrors = true;
						status = "KO " + e.getMessage();
					}
					mapFileStatus.put(filename, status);
				}
				addLogFileIntoZip(root, ptrNumber, mapFileStatus, zos);
				zos.close();
				fos.close();
				hasBeenWritten = true;
			} catch (FileNotFoundException fnfe) {
				if (fnfe.getMessage().contains(FILE_ALREADY_USED)) {
					Log.log("This file " + zipFile + " is already opened, please close it.", Log.W);
					getInputUtil().getUserInput("Please correct situation and press any key to check again");
				} else {
					Log.log("Unkown error blocking the application " + fnfe.getMessage(), Log.E);
					throw new WhatTheFuckException(Log.Exit.FILE_NOT_EXIST);
				}
			}
		} while (!hasBeenWritten);
		if (hasErrors == null) {
			Log.log("Zip has not been created", Log.E);
			throw new WhatTheFuckException(Log.Exit.ZIP_NOT_CREATED);
		} else if (hasErrors) {
			Log.log("Zip has been created with some errors", Log.E);
			throw new WhatTheFuckException(Log.Exit.ZIP_CREATION_ERROR);
		} else {
			Log.log("Zip has been created succesfully", Log.I);
		}
	}

	/**
	 * Add a Log.log file at the root of the zip file, with all files and their
	 * status.
	 * 
	 * @param root
	 * @param chosenFiles
	 * @throws IOException
	 */
	public void addLogFileIntoZip(String root, String ptrNumber, Map<String, String> chosenFilesWithStatus,
			ZipOutputStream zos) throws Exception {
		Log.log("Writing '" + logFilename + "' to zip file", Log.I);
		StringBuilder strBuilder = new StringBuilder();
		strBuilder.append("This zip file has been generated with CodeReviewGenerator\n");
		strBuilder.append("Has been created by: " + System.getProperty("user.name") + "\n");
		strBuilder.append("From this machine: " + InetAddress.getLocalHost().getHostName() + "\n");
		strBuilder.append("Considering this local stream: " + root + "\n");
		strBuilder.append("And this activity: " + ptrNumber + "\n");
		strBuilder.append("Has been created: " + Utils.getCurrentDateTime() + "\n\n");
		strBuilder.append("List of files to be included:\n");
		for (Map.Entry<String, String> fileWithStatus : chosenFilesWithStatus.entrySet()) {
			String relativePath = getRelativePath(root, fileWithStatus.getKey());
			strBuilder.append(relativePath + "\t[" + fileWithStatus.getValue() + "]\n");
		}
		String strLog = strBuilder.toString();
		// Since we don't want to generate zip file with absolute path into, but
		// only relative, we call getRelativePath()
		Log.log("We get this log file:\n" + strLog, Log.D);
		ZipEntry zipEntry = new ZipEntry(logFilename);
		zos.putNextEntry(zipEntry);
		zos.write(strLog.getBytes(), 0, strLog.length());
		zos.closeEntry();
	}

	/**
	 * Add a given file into the given zip file
	 * 
	 * @param root
	 * @param fileName
	 * @param zos
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void addToZipFile(String root, String fileName, ZipOutputStream zos) throws Exception {
		Log.log("Writing '" + fileName + "' to zip file", Log.D);
		File file = new File(fileName);
		FileInputStream fis = new FileInputStream(file);
		// Since we don't want to generate zip file with absolute path into, but
		// only relative, we call getRelativePath()
		ZipEntry zipEntry = new ZipEntry(getRelativePath(root, fileName));
		zos.putNextEntry(zipEntry);
		byte[] bytes = new byte[CLUSTER_ZIP_FILE];
		int length;
		while ((length = fis.read(bytes)) >= 0) {
			zos.write(bytes, 0, length);
		}
		zos.closeEntry();
		fis.close();
	}

	/**
	 * Extract a given file from a given zipFile.
	 * 
	 * @param zippedFile
	 * @param name
	 * @return
	 */
	public String readFileInZip(File zippedFile, String name) throws Exception {
		StringBuilder strUnzipped = new StringBuilder();
		ZipFile zf = null;
		try {
			zf = new ZipFile(zippedFile);
			Enumeration<? extends ZipEntry> zentries = zf.entries();
			while (zentries.hasMoreElements()) {
				ZipEntry ze = zentries.nextElement();
				Log.log("Unzipping " + ze.getName(), Log.D);
				if (ze.getName().equals(name)) {
					InputStream stream = zf.getInputStream(ze);
					Reader in = new InputStreamReader(stream, "UTF-8");
					final char[] buffer = new char[BUFFER_FILE];
					for (;;) {
						int rsz = in.read(buffer, 0, buffer.length);
						if (rsz < 0)
							break;
						strUnzipped.append(buffer, 0, rsz);
					}
					in.close();
					stream.close();
				}
			}
		} catch (FileNotFoundException e) {
			Log.log("Could not find " + name + " from " + zippedFile.getAbsolutePath() + " " + e.getMessage(), Log.E);
			throw new WhatTheFuckException(Log.Exit.FILE_NOT_EXIST);
		} catch (IOException e) {
			Log.log("Issue while unzipping file " + name + " from " + zippedFile.getAbsolutePath() + " "
					+ e.getMessage(), Log.E);
			throw new WhatTheFuckException(Log.Exit.IO_EXCEPTION);
		} finally {
			try {
				zf.close();
			} catch (IOException e) {
				Log.log("Issue while closing fileReader for  " + zippedFile.getAbsolutePath() + " " + e.getMessage(),
						Log.D);
			}
		}
		Log.log("Zipped file is [" + strUnzipped + "]", Log.DD);
		return strUnzipped.toString();
	}

	/**
	 * Ensure we have an empty file from the beginning, erasing the already
	 * existing one if needed
	 * 
	 * @param file
	 * @return
	 */
	protected File replaceFileIfExists(File file) throws Exception {
		File newFile;
		if (file.exists()) {
			Log.log("We delete already existing " + file, Log.I);
			file.delete();
			newFile = new File(file.getAbsolutePath());
		} else {
			newFile = file;
		}
		return newFile;
	}

	/**
	 * Provide the relative path for a given file, with root as source folder.
	 * It is mandatory the file is into this root folder. We need to take care
	 * because sometimes we are considering folders where we don't have any "\"
	 * at the beginning, and the log files where we have it
	 * 
	 * @param root
	 * @param absoluteFile
	 * @return
	 */
	protected String getRelativePath(String root, String absoluteFile) throws Exception {
		// We check the file starts with the same path as the root one
		if (absoluteFile.startsWith(root)) {
			int prefixSize = root.length();
			String relativeFile = absoluteFile.substring(prefixSize + 1);
			Log.log("getRelativePath for " + absoluteFile + " with root " + root + ": " + relativeFile, Log.D);
			return relativeFile;
		}
		// If not, we throw an exception to don't process this file
		Log.log("The file " + absoluteFile + "is not stored in root " + root, Log.W);
		throw new UnsupportedOperationException();
	}

	public void unzipFile(String zipFile, String outputFolder) throws Exception {
		byte[] buffer = new byte[CLUSTER_ZIP_FILE];
		// create output directory is not exists
		File folder = new File(outputFolder);
		if (!folder.exists()) {
			folder.mkdir();
		}
		// get the zip file content
		ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
		// get the zipped file list entry
		ZipEntry ze = zis.getNextEntry();
		while (ze != null) {
			String fileName = ze.getName();
			File newFile = new File(outputFolder + File.separator + fileName);
			if (newFile.exists() && !newFile.canWrite()) {
				Log.log("activate write on this file " + newFile, Log.D);
				newFile.setWritable(true);
				Log.log("file unzip : " + newFile.getAbsoluteFile(), Log.D);
			}
			// create all non exists folders
			// else you will hit FileNotFoundException for compressed folder
			new File(newFile.getParent()).mkdirs();
			FileOutputStream fos = new FileOutputStream(newFile);
			int len;
			while ((len = zis.read(buffer)) > 0) {
				fos.write(buffer, 0, len);
			}
			fos.close();
			ze = zis.getNextEntry();
		}
		zis.closeEntry();
		zis.close();
		Log.log("Unzip process done", Log.D);
	}

	/**
	 * @return the inputUtil
	 */
	protected UserInputHandler getInputUtil() {
		return inputUtil;
	}
}
